package gm.tcpping;

import gm.tcpping.ntp.SntpClient;

import java.io.*;
import java.net.*;
import java.text.SimpleDateFormat;
import java.util.*;

public final class Pitcher {
    
    // ===========================================================
    // Constants
    // ===========================================================

    private static final long PRINT_INTERVAL = 1000;
    private static final int NTP_OFFSET_MESSAGE_SIZE = 14;
    
    private static final int TIMEOUT_INTERVAL = 5000;
    
    // ===========================================================
    // Fields
    // ===========================================================
    
    private final int mPort;
    private final int mMessagesPerSecond;
    private final int mMessageSize;
    private final String mHostname;
    
    private Socket mSocket;
    private BufferedInputStream mInput;
    private OutputStream mOutput;
    
    private long mCurrentMessageIndex;
    private long mSendInterval;
    
    // messages that have been sent to pitcher but not yet returned
    private HashMap<Long, Long> mPendingMessages;
    
    // messages received since last print
    private ArrayList<Message> mReceivedMessages;
    
    private long mPToCTotalMax;
    private long mCToPTotalMax;
    private long mRTTTotalMax;

    // ===========================================================
    // Constructors
    // ===========================================================

    public Pitcher(
            int port,
            int messagesPerSecond,
            int messageSize,
            String hostname) {
        mPort = port;
        mMessagesPerSecond = messagesPerSecond;
        mMessageSize = messageSize;
        mHostname = hostname;
        
        mSocket = null;
        mInput = null;
        mOutput = null;
        
        mCurrentMessageIndex = 0;
        mSendInterval = (long)(1000.0 / mMessagesPerSecond);
        
        mPendingMessages = null;
        mReceivedMessages = null;
        
        mPToCTotalMax = 0;
        mCToPTotalMax = 0;
        mRTTTotalMax = 0;
    }
    
    // ===========================================================
    // Methods for/from SuperClass/Interfaces
    // ===========================================================

    // ===========================================================
    // Methods
    // ===========================================================
    
    public void start() {
        stop();
        
        mPendingMessages = new HashMap<Long, Long>();
        mReceivedMessages = new ArrayList<Message>();
        
        mPToCTotalMax = 0;
        mCToPTotalMax = 0;
        mRTTTotalMax = 0;
        
        try {
            InetAddress address = InetAddress.getByName(mHostname);
            mSocket = new Socket(address, mPort);

            byte[] inputBuffer = new byte[TCPPing.BUFFER_SIZE];
            mInput = new BufferedInputStream(mSocket.getInputStream(), TCPPing.BUFFER_SIZE);
            mOutput = mSocket.getOutputStream();
            
            sendMessageSize();
            
            long catcherNTPOffset = recieveNTPOffset(inputBuffer);
            long pitcherNTPOffset = SntpClient.getNTPOffset();
            
            // this is what we need to add to catcher time to synchronize it with pitcher time
            long cpOffset = catcherNTPOffset - pitcherNTPOffset;
            
            long lastSendTime = System.currentTimeMillis();
            long lastPrintTime = System.currentTimeMillis();
            while (true) {
                long currTime = System.currentTimeMillis();
                
                if (currTime - lastSendTime >= mSendInterval) {
                    Message message = new Message(mCurrentMessageIndex, mMessageSize, currTime);
                    mPendingMessages.put(mCurrentMessageIndex, currTime);
                    mOutput.write(Message.serialize(message));
                    
                    mCurrentMessageIndex++;
                    lastSendTime = currTime;
                }
                
                while (mInput.available() >= mMessageSize) {
                    Message message = recieveMessage(inputBuffer);
                    long messageId = message.getMessageId();
                    
                    // we only add pending messages to the list of received messages
                    // if it is not in list of pending messages it is simply ignored
                    // (this can happen if timed out message arrives late)
                    if (mPendingMessages.containsKey(messageId)) {
                        message.setPitcherReturnTime(currTime);
                        message.offsetCatcherTime(cpOffset);
                        mPendingMessages.remove(messageId);
                        mReceivedMessages.add(message);
                    }
                }
                
                if (currTime - lastPrintTime >= PRINT_INTERVAL) {
                    handleStatusPrint(currTime);
                    lastPrintTime = currTime;
                }
                
                handleTimeouts(currTime);
            }
            
        } catch (IOException e) {
            System.out.println(e);
        } catch (IllegalArgumentException e) {
            System.out.println(e);
        } catch (SecurityException e) {
            System.out.println(e);
        } catch (ClassNotFoundException e) {
            System.out.println(e);
        }
    }
    
    public void stop() {
        try {
            if (mOutput != null) {
                mOutput.close();
                mOutput = null;
            }
            if (mInput != null) {
                mInput.close();
                mInput = null;
            }
            if (mSocket != null) {
                mSocket.close();
                mSocket = null;
            }
        } catch (IOException e) {
            System.out.println(e);
        }
    }
    
    private void sendMessageSize() throws IOException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeShort(mMessageSize);
        oos.flush();
        byte[] buf = bos.toByteArray();
        
        mOutput.write(buf);
    }
    
    private long recieveNTPOffset(byte[] inputBuffer) throws IOException {
        int bytesRead = 0;
        while (bytesRead < NTP_OFFSET_MESSAGE_SIZE) {
            bytesRead += mInput.read(inputBuffer, bytesRead, NTP_OFFSET_MESSAGE_SIZE - bytesRead);
        }
        
        ByteArrayInputStream bis = new ByteArrayInputStream(inputBuffer);
        ObjectInputStream ois = new ObjectInputStream(bis);
        long ntpOffset = ois.readLong();
        ois.close();
        bis.close();
        
        return ntpOffset;
    }
    
    private Message recieveMessage(byte[] inputBuffer) throws IOException, ClassNotFoundException {
        int bytesRead = 0;
        while (bytesRead < mMessageSize) {
            bytesRead += mInput.read(inputBuffer, bytesRead, mMessageSize - bytesRead);
        }
        
        Message message = Message.deserialize(inputBuffer);
        return message;
    }
    
    private void handleStatusPrint(long currTime) {
        
        Date currDate = new Date(currTime);
        SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
        
        mPToCTotalMax = Math.max(mPToCTotalMax, getMaxPToC());
        mCToPTotalMax = Math.max(mCToPTotalMax, getMaxCToP());
        mRTTTotalMax = Math.max(mRTTTotalMax, getMaxRTT());
        
        String statusText = String.format(
                "%s - Total Sent: %d - Received: %d - Avg P->C: %s - Avg C->P: %s - Avg RTT: %s - " +
                "Total Max P->C: %s - Total Max C->P: %s - Total Max RTT: %s",
                sdf.format(currDate),
                mCurrentMessageIndex,
                mReceivedMessages.size(),
                formatMsTime(getAvgPToC()),
                formatMsTime(getAvgCToP()),
                formatMsTime(getAvgRTT()),
                formatMsTime(mPToCTotalMax),
                formatMsTime(mCToPTotalMax),
                formatMsTime(mRTTTotalMax));
        
        System.out.println(statusText);
        
        mReceivedMessages.clear();
    }
    
    private void handleTimeouts(long currTime) {
        Iterator<Map.Entry<Long, Long>> it = mPendingMessages.entrySet().iterator();
        while (it.hasNext()) {
            Map.Entry<Long, Long> entry = it.next();
            if (currTime - entry.getValue() > TIMEOUT_INTERVAL) {
                System.out.format("Warning: Message %d timed out.%n", entry.getKey());
                it.remove();
            }
        }
    }
    
    private long getAvgPToC() {
        long sum = 0;
        if (mReceivedMessages.isEmpty()) {
            return -1;
        }
        
        for (Message m : mReceivedMessages) {
            sum += m.getPitcherToCatcher();
        }
        
        long avg = Math.round((double)sum / (double)mReceivedMessages.size());
        return Math.max(0, avg);
    }
    
    private long getAvgCToP() {
        long sum = 0;
        if (mReceivedMessages.isEmpty()) {
            return -1;
        }
        
        for (Message m : mReceivedMessages) {
            sum += m.getCatcherToPitcher();
        }
        
        long avg = Math.round((double)sum / (double)mReceivedMessages.size());
        return Math.max(0, avg);
    }
    
    private long getAvgRTT() {
        long sum = 0;
        if (mReceivedMessages.isEmpty()) {
            return -1;
        }
        
        for (Message m : mReceivedMessages) {
            sum += m.getRTT();
        }
        
        long avg = Math.round((double)sum / (double)mReceivedMessages.size());
        return Math.max(0, avg);
    }
    
    private long getMaxPToC() {
        long max = 0;
        for (Message m : mReceivedMessages) {
            max = Math.max(m.getPitcherToCatcher(), max);
        }
        return max;
    }
    
    private long getMaxCToP() {
        long max = 0;
        for (Message m : mReceivedMessages) {
            max = Math.max(m.getCatcherToPitcher(), max);
        }
        return max;
    }
    
    private long getMaxRTT() {
        long max = 0;
        for (Message m : mReceivedMessages) {
            max = Math.max(m.getRTT(), max);
        }
        return max;
    }
    
    private String formatMsTime(long time) {
        if (time < 0) return "-";
        else if (time < 1) return "<1ms";
        else return "" + time + "ms";
    }
}